package com.demarc.pesquisas.infraestrutura.web.component;

import static java.lang.Boolean.FALSE;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import javax.faces.application.FacesMessage;
import javax.faces.component.FacesComponent;
import javax.faces.component.UIComponent;
import javax.faces.component.UIForm;
import javax.faces.component.UIInput;
import javax.faces.component.UISelectBoolean;
import javax.faces.context.FacesContext;

import org.omnifaces.component.validator.ValidatorFamily;
import org.omnifaces.util.Components;
import org.omnifaces.util.State;
import org.omnifaces.util.Utils;

import com.demarc.pesquisas.infraestrutura.mensagem.MensagemJsfUtils;

@FacesComponent(ValidacaoTodosSemInvalidar.COMPONENT_TYPE)
public class ValidacaoTodosSemInvalidar extends ValidatorFamily {

	public static final String VALIDACAO_TODOS_SEM_INVALIDAR_MESSAGE_ID = "com.demarc.pesquisas.infraestrutura.web.component.ValidacaoTodosSemInvalidar.VALIDACAOTODOS";

	// Private constants
	// ----------------------------------------------------------------------------------------------

	private static final String DEFAULT_SHOWMESSAGEFOR = "@all";
	private static final Boolean DEFAULT_DISABLED = FALSE;

	private static final String ERROR_MISSING_COMPONENTS = "%s attribute 'components' must be specified.";
	private static final String ERROR_UNKNOWN_COMPONENT = "%s attribute 'components' must refer existing client IDs. Client ID '%s' cannot be found.";
	private static final String ERROR_INVALID_COMPONENT = "%s attribute 'components' must refer UIInput client IDs. Client ID '%s' is of type '%s'.";
	private static final String ERROR_INVALID_SHOWMESSAGEFOR = "%s attribute 'showMessageFor' must be '@this' or '@all'. Encountered invalid value '%s'.";

	private enum PropertyKeys {
		// Cannot be uppercased. They have to exactly match the attribute names.
		components, message, showMessageFor, disabled;
	}

	// Variables
	// ------------------------------------------------------------------------------------------------------

	private final State state = new State(getStateHelper());

	// Properties
	// -----------------------------------------------------------------------------------------------------

	private final String defaultMessage;
	private boolean validationFailed;

	// Constructors
	// ---------------------------------------------------------------------------------------------------

	/**
	 * The default constructor sets the default message and sets the renderer
	 * type to <code>null</code>.
	 */
	public ValidacaoTodosSemInvalidar() {
		this.defaultMessage = VALIDACAO_TODOS_SEM_INVALIDAR_MESSAGE_ID;
		setRendererType(null);
	}

	// Actions
	// --------------------------------------------------------------------------------------------------------

	/**
	 * Validate our component hierarchy.
	 * 
	 * @throws IllegalArgumentException
	 *             When there is no parent of type {@link UIForm}, or when there
	 *             are any children.
	 */
	@Override
	protected void validateHierarchy() throws IllegalArgumentException {
		Components.validateHasParent(this, UIForm.class);
		Components.validateHasNoChildren(this);
	}

	/**
	 * If the validation is not disabled, collect the components, if it is not
	 * empty, then collect their values and delegate to
	 * {@link #validateValues(FacesContext, List, List)}. If it returns
	 * <code>false</code>, then mark all inputs and the faces context invalid
	 * and finally delegate to {@link #showMessage(FacesContext, List)} to show
	 * the message.
	 */
	@Override
	protected void validateComponents(FacesContext context) {
		if (isDisabled()) {
			return;
		}

		List<UIInput> inputs = collectComponents();

		if (inputs.isEmpty()) {
			return;
		}

		List<UIInput> invalidInputs = new ArrayList<UIInput>();

		for (UIInput input : inputs) {
			Object value = Components.getValue(input);

			if (input instanceof UISelectBoolean && Boolean.FALSE.equals(value)) {
				value = null;
			}

			if (Utils.isEmpty(value)) {
				invalidInputs.add(input);
				input.setValid(false);
			}
		}

		if (!invalidInputs.isEmpty()) {
			validationFailed = true;
			context.validationFailed();
			showMessage(context, invalidInputs);
		}
	}

	/**
	 * Collect the input components. Only those which are an instance of
	 * {@link UIInput}, are rendered, not disabled nor readonly will be
	 * returned. If at least one of them has already been validated and is been
	 * marked invalid, then an empty collection will be returned.
	 * 
	 * @return The input components.
	 * @throws IllegalArgumentException
	 *             When the <code>components</code> attribute is missing, or
	 *             when it references an non-existing component, or when it
	 *             references a non-input component.
	 */
	protected List<UIInput> collectComponents() {
		String components = getComponents();

		if (components.isEmpty()) {
			throw new IllegalArgumentException(String.format(
					ERROR_MISSING_COMPONENTS, getClass().getSimpleName()));
		}

		UIComponent namingContainerParent = getNamingContainer();
		List<UIInput> inputs = new ArrayList<UIInput>();

		for (String clientId : components.split("\\s+")) {
			UIComponent found = namingContainerParent.findComponent(clientId);

			if (found == null) {
				throw new IllegalArgumentException(String.format(
						ERROR_UNKNOWN_COMPONENT, getClass().getSimpleName(),
						clientId));
			} else if (!(found instanceof UIInput)) {
				throw new IllegalArgumentException(String.format(
						ERROR_INVALID_COMPONENT, getClass().getSimpleName(),
						clientId, found.getClass().getName()));
			}

			UIInput input = (UIInput) found;

			if (!Components.isEditable(input)) {
				continue;
			}

			if (!input.isValid()) {
				return Collections.emptyList();
			}

			inputs.add(input);
		}

		return inputs;
	}

	/**
	 * Show the message at the desired place(s) depending on the value of the
	 * <code>showMessageFor</code> attribute.
	 * <ul>
	 * <li><code>@this</code>: message will be added to the
	 * <code>&lt;h:message&gt;</code> for this component.
	 * <li><code>@all</code>: message will be added to all components as
	 * specified in <code>components</code> attribute.
	 * </ul>
	 * 
	 * @param context
	 *            The faces context to work with.
	 * @param inputs
	 *            The validated input components.
	 */
	protected void showMessage(FacesContext context, List<UIInput> inputs) {
		String message = getMessage();
		String showMessageFor = getShowMessageFor();

		if (showMessageFor.equals("@this")) {
			StringBuilder labels = new StringBuilder();
			for (Iterator<UIInput> iterator = inputs.iterator(); iterator
					.hasNext();) {
				UIInput input = iterator.next();
				labels.append(Components.getLabel(input));

				if (iterator.hasNext()) {
					labels.append(", ");
				}
			}
			MensagemJsfUtils.adicionarMensagem(getClientId(context),
					MensagemJsfUtils.criarFacesMessage(
							FacesMessage.SEVERITY_ERROR, message, labels));
		} else if (showMessageFor.equals("@all")) {
			for (UIInput input : inputs) {
				MensagemJsfUtils.adicionarMensagem(input.getClientId(context),
						MensagemJsfUtils.criarFacesMessage(
								FacesMessage.SEVERITY_ERROR, message,
								Components.getLabel(input)));
			}
		} else {
			throw new IllegalArgumentException(String.format(
					ERROR_INVALID_SHOWMESSAGEFOR, getClass().getSimpleName(),
					showMessageFor));
		}
	}

	// Attribute getters/setters
	// --------------------------------------------------------------------------------------

	/**
	 * Returns the client identifiers of components which are to be validated.
	 * 
	 * @return The client identifiers of components which are to be validated.
	 */
	public String getComponents() {
		return state.get(PropertyKeys.components, "");
	}

	/**
	 * Sets the client identifiers of components which are to be validated.
	 * 
	 * @param components
	 *            The client identifiers of components which are to be
	 *            validated.
	 */
	public void setComponents(String components) {
		state.put(PropertyKeys.components, components);
	}

	/**
	 * Returns the validation message to be shown.
	 * 
	 * @return The validation message to be shown.
	 */
	public String getMessage() {
		return state.get(PropertyKeys.message, defaultMessage);
	}

	/**
	 * Sets the validation message to be shown.
	 * 
	 * @param message
	 *            The validation message to be shown.
	 */
	public void setMessage(String message) {
		state.put(PropertyKeys.message, message);
	}

	/**
	 * Returns the client identifier to show the validation message for.
	 * 
	 * @return The client identifier to show the validation message for.
	 */
	public String getShowMessageFor() {
		return state.get(PropertyKeys.showMessageFor, DEFAULT_SHOWMESSAGEFOR);
	}

	/**
	 * Sets the client identifier to show the validation message for.
	 * 
	 * @param showMessageFor
	 *            The client identifier to show the validation message for.
	 */
	public void setShowMessageFor(String showMessageFor) {
		state.put(PropertyKeys.showMessageFor, showMessageFor);
	}

	/**
	 * Returns whether the validation should be disabled or not.
	 * 
	 * @return Whether the validation should be disabled or not.
	 */
	public Boolean isDisabled() {
		return state.get(PropertyKeys.disabled, DEFAULT_DISABLED);
	}

	/**
	 * Sets whether the validation should be disabled or not.
	 * 
	 * @param disabled
	 *            Whether the validation should be disabled or not.
	 */
	public void setDisabled(Boolean disabled) {
		state.put(PropertyKeys.disabled, disabled);
	}

	/**
	 * Returns whether the validation has failed or not.
	 * 
	 * @return Whether the validation has failed or not.
	 * @since 1.3
	 */
	public boolean isValidationFailed() {
		return validationFailed;
	}

	// Public constants
	// -----------------------------------------------------------------------------------------------

	/** The standard component type. */
	public static final String COMPONENT_TYPE = "com.demarc.pesquisas.infraestrutura.web.component.ValidacaoTodosSemInvalidar";

	// Actions
	// --------------------------------------------------------------------------------------------------------
}